<?php
/**
 * Модель
 * 
 * Общий класс модели
 * @author Anton Zavidov
 * @package myLiveShop
 */

import('core.field');
/**
 * Базовый класс для работы с моделями
 *
 */
abstract class Model extends Base {
	
    /**
	 * Прямой тип сортировки
	 *
	 */
	const ASC = 'ASC';
	/**
	 * Обратный тип сортировки
	 *
	 */
	const DESC = 'DESC';
	/**
	 * база данных
	 *
	 * @var DB
	 */
	protected $db;
	/**
	 * Имя таблицы
	 *
	 * @var string
	 */
	protected $name;
	/**
	 * Ключевые поля
	 *
	 * @var array;
	 */
	protected $id_field;
	/**
	 * Массив полей
	 *
	 * @var array
	 */
	protected $fields;
	
	/**
	 * Количество строк после последнего запроса
	 *
	 * @var int
	 */
	public $totall;
	
	/**
	 * Пейджер
	 *
	 * @var Pagination
	 */
	public $pagination;
	/**
	 * Констуктор
	 *
	 * @param string $className имя подели
	 */
	public function __construct($className=null){
		$this->db=Mysql::getInstance();
		parent::__construct($className?$className:__CLASS__);
	}
	public function set(){
		
	}
	/**
	 * Загружаем данные
	 *
	 * @param mixed $id array / string значения ключевых полей
	 * @param mixed $fieldList array / string список необходимых полейб по умолчанию берутся все
	 * @param array $params дополнительные параметры для выборки join, order, group, limit, where
	 * @return array результат выборки
	 */
	public function load($id=false,$fieldList=false,$params=array()){
		$fields=array();
		if(isset($params['fields'])) $fields = $params['fields'];
		else {
			//если $fieldList передан
			if ($fieldList!==false && $fieldList!="*"){
				//превращаем в массив даже если это строка
				toArray($fieldList);
			}else{
				//если н передан берём список всех полей
				$fields=$this->getField();
			}
			//если передали массив полей
			if(is_array($fieldList)){
				//проходимся по списку
				foreach ($fieldList as $item) {
					//если в названии находим точку то ищем имя поля и название таблицы
					if(strpos($item,DOT)){
						$item=explode(DOT,$item);
						$field=$this->getField($item[1],$item[0]);
						//ищем join поля
						//если поле не из этой таблици
						if($item[0]!=$this->name && (!isset($params['join']) || (isset($params['join']) && !$this->findExistJoinField($params['join'],$item)))){
							$this->findJoinField($params['join'],$item);
						}
					}else{
						$field=$this->getField($item);
					}
					if(!is_array($field) && $field!==false){
						$fields[]=$field;	
					}elseif(strpos($item,' AS ')){
						$fields[]=$item;
					}
				}
			}
		}
		$where=array();
		//собираем where запрос на основе id полей и их знчений
		if($id){
		    $id_list=$this->id_field;
		    toArray($id);
		    //если передан числовой массив
		    if(isset($id[0])){
		        foreach ($id_list as $key=>$item) {
		            if(!isset($id[$key])){
		                new CoreExeption('Количество id полей не сответствует нужному количеству ',E_WARNING,__FILE__,__LINE__,array(
		                'id'=>$id,
		                'id_field'=>$id_list
		                ));
						return null;
		            }
		        	$item=$this->getField($item);
		        	if($item===false){
						new CoreExeption('Не найдено ключевое поле '.$item.', в модели '.$this->name,E_WARNING,__FILE__,__LINE__);
						return null;
					}
					$field=&$item;
    				$this->db->eraseData($id[$key]);
    			    if(!is_callable(array($field,'fieldName'))){
    			        new CoreExeption('Не найдено fieldName()',E_ERROR,__FILE__,__LINE__,array('field'=>$field));
    			        continue;
    			    }
    			    $where[]=$field->fieldName().' = '.$id[$key];
		        }
		    }else{
		      //если передан асоциативный массив
			 foreach ($id as $key=>$item){
    				if($item===false)continue;
    				if(is_string($id_list[$key])){
    					$id_list[$key]=$this->getField($id_list[$key]);
    					if($id_list[$key]===false){
    						new CoreExeption('Не найдено ключевое поле '.$id_list[$key].', в модели '.$this->name,E_WARNING,__FILE__,__LINE__);
    						return null;
    					}
    					
    				}
    				$field=&$id_list[$key];
    				$this->db->eraseData($item);
    			    if(!is_callable(array($field,'fieldName'))){
    			        new CoreExeption('Не найдено fieldName()',E_ERROR,__FILE__,__LINE__,array('field'=>$field));
    			        continue;
    			    }
    			    $where[]=$field->fieldName().' = '.$item;
    			}
		    }
		}
		$where=implode(' AND ',$where).(isset($params['where'])?' '.$params['where']:'');
		
		$build_array=array(
			'fields'=>$fields,
			'table'=>DB::apostrof($this->name),
			'join'=>isset($params['join'])?$params['join']:'',
			'order'=>isset($params['order'])?$params['order']:'',
			'group'=>isset($params['group'])?$params['group']:'',
			'limit'=>isset($params['limit'])?$params['limit']:'',
			'where'=>$where
		);
		
		//учитываем страници
		if(isset($params['pagination'])){
			import('core.filterinput');
			$totall_array=$build_array;
			$totall_array['limit']=1;
			$totall_array['fields']='COUNT(*)';
			$totall_array['order']=false;
			$this->totall=$this->db->getArray($this->db->buildQuery($totall_array),true,$totall_array['fields']);
			$page=isset($params['pagination']['page'])&&$params['pagination']['page']?$params['pagination']['page']:1;
			$show_limit=isset($params['pagination']['limit'])?$params['pagination']['limit']:Config::_('default_limit');
			$page=Filterinput::clean($page,'int');
			$show_limit=Filterinput::clean($show_limit,'int');
			$page_offset=($page-1)*$show_limit;
			if($page_offset>=$this->totall){
				$page=ceil($this->totall/$show_limit);
				$page_offset=floor($this->totall/$show_limit)*$show_limit;
			}
			if(isset($params['pagination']['show'])){
				import('core.pagination');
				$this->pagination=new Pagination($this->totall,$page,$show_limit);
			}
			$build_array['limit']=($page_offset>0?$page_offset.', ':'').$show_limit;
		}
		//строим запрос
		$sql=$this->db->buildQuery($build_array);
		
		//выполняем запрос
		//если передано одно поле и стоит установка возвращать одно поле то его вернут по умолчанию
		if(isset($params['one_field']) && $params['one_field'] && count($fields)==1){
			$params['one_field']=$this->fieldByName($fields[0]);
		}
		$result=$this->db->getArray($sql,isset($params['one_row'])?$params['one_row']:null,isset($params['one_field'])?$params['one_field']:null);
		return $result;
	}
	/**
	 * Загружаем данные одной строкой
	 * 
	 * Автоматически добавляем one_row=true и limit=1
	 *
	 * @param mixed $id array / string значения ключевых полей
	 * @param mixed $fieldList array / string список необходимых полейб по умолчанию берутся все
	 * @param array $params дополнительные параметры для выборки join, order, group, limit, where
	 * @return array результат выборки
	 */
	public function loadOne($id,$fieldList=false,$params=array()){
		//toArray($fieldList);
		$paramsDeault=array(
			'one_row'=>true,
			'limit'=>1
		);
		//если не переданно более одного поля на получение ине передана звёздочка то возвращем значение как стоку из одного поля
		if(count($fieldList)==1 && !isset($params['one_field']) && $fieldList!==false && $fieldList!='*'){
			$paramsDeault['one_field']=$this->fieldByName($fieldList[0]);
		}
		$params=array_merge($paramsDeault,$params);
		//загружаем поля
		return $this->load($id,$fieldList,$params);
	}
	/**
	 * Получаем число элементов по запросу
	 *
	 * @param array $params дополнительные параметры для выборки table, join, order, group, where
	 * @return int
	 */
	public function loadCount($params=array()){
	    $params['limit']=1;
	    if(!isset($params['fields']))$params['fields']='COUNT(*)';
	    if(!isset($params['table']))$params['table']=DB::apostrof($this->name);
	    return (int) $this->db->getArray($this->db->buildQuery($params),true,$params['fields']);
	}
	/**
	 * Ищем есть ли поле в предустановках join
	 *
	 * @param array $join_string ссылка на список join строк
	 * @param array $field_data array(table,field) подключаемые таблица и поле
	 */
	protected function findExistJoinField(&$join_string,$field_data){
		foreach ($join_string as $item) {
			if($item['table'] == $field_data[0]) return true;
		}
		return false;
	}
	/**
	 * Ищем соотношения в join полях
	 *
	 * @param array $join_string ссылка на список join строк
	 * @param array $field_data array(table,field) подключаемые таблица и поле
	 */
	protected function findJoinField(&$join_string,$field_data){
		$return=array();
		//получаем нужное нам поле
		if($field=$this->getField($field_data[1],$field_data[0])){
			$return=array(
				'table'=>		DB::apostrof($field_data[0]),
				'key'=>			DB::apostrof(!is_string($field->model->id_field[0])?$field->model->id_field[0]->_('name'):$field->model->id_field[0]),
				'join_table'=>	DB::apostrof($this->name));
			foreach ($this->fields as $item) {
				if($item->_('join_table')==$field_data[0]){
					$return['join_field']=DB::apostrof($item->_('join_field'));
					break;
				}
			}
			if(!is_array($join_string))$join_string=array();
			$join_string[]=$return;
		}
	}
	/**
	 * Собираем поля для запроса в массив строк
	 *
	 * @param array $fields массив полей или строк
	 * @return array массив строк 
	 */
	protected function groupFields($fields){
		$return=array();
		foreach ($fields as $item) {
			if(!is_string($item)){
				$item=$item->fieldName();
			}
			$return[]=$item;
		}
		return $return;
	}
	/**
	 * Получаем поле либо все поля таблицы
	 *
	 * @param string $name имя поля (если не указанно то вернёт все)
	 * @param string $table имя таблицы (если не указанно то возмёт имя текущей таблици)
	 * @return array or Field or false если поле не найденно
	 */
	public function getField($name=false,$table=null){
		if(!$name)return $this->fields;
		$from=$this;
		//если таблица не передана берём её из имени
		if(strpos($name,' AS ')>0){
		    $name=explode(' AS ',$name);
		    $name=$name[0];
		}
		if($table===null)$table=$this->name;
		//ели передана добавляем точку
		elseif(!$table)$table='';
		else{
			if($table!=$this->name){
				$modelName='Model'.ucfirst($table);
				$from=import('model.'.$table,true);
			}
		}
		if(isset($from->fields[$table.DOT.$name])){
			return $from->fields[$table.DOT.$name];
		}else{
			new CoreExeption('Не удалось найти поле '.$table.$name.', в модели '.$this->name,E_WARNING,__FILE__,__LINE__);
			return false;
		}
	}
	/**
	 * Получаем поле по его имени
	 *
	 * @param mixed $name имя поля
	 * @return string
	 */
	public function fieldByName($name){
		if(!is_string($name)){
			if($name instanceof Field)return $name->_('name');
			new CoreExeption('Попытка получить неизвестное поле в модели '.$this->name,E_ERROR,__FILE__,__LINE__,array('name'=>$name));
			return null;
		}
		return $name;
	}
	/**
	 * Сохраняем двнные
	 *
	 * @param array $data массив данных где ключ это имя поля
	 * @param mixed $id число или массив указывающий на id для перезаписи
	 * @param array $params дополнительные параметры
	 * @return mixed true при обновлении или новый id при добавлении
	 */
	public function save($data,$id=false,$params=false){
		$errors=array();
		$list=array();
		//проходим по всем полям и записываем в них данные
		foreach ($this->getField() as $name=>$field) {
			//ищем поле по имени
			if(isset($data[$name])){
				$value=$data[$name];				
			}elseif(isset($data[$field->_('name')])){
				$value=$data[$field->_('name')];
			}elseif(!$id && !isset($params['where'])){
				$value=null;
			}else{
				//если update то не добавляем лишних полей
				continue;
			}
			//если поле ещё не было установлено или значение передано для этого поля
			if(!$field->_('validated') || $value!==null){
				//очищаем поля пред записью
				$field->erase();
				//устанавливаем значение поля
				$field->value($value,true);
			}
			//последняя проверка перед записью
			$result=$field->preSave();
			//если проверка успешная то добавлем в массив на запись
			if($result===true){
				if(!$field->_('custom') && (!$field->_('auto') || ($field->_('auto') && $value))){
					
					$list[$id?$field->fieldName():$field->_('name')]=$field->value();
				}
			}else{
				//иначе записываем ошибку
				$errors[$name]=$result;
			}
		}
		if(count($errors))return $errors;
		//делаем update если есть id или какие нибудь другие условия
		if($where=$this->createWhere($id,$params)){
			//если у модели находим собственный update делаем его
			if(method_exists($this,'update'))return $this->update($list,$where,$params);
			//иначе запускаем нативный update
			return $this->db->update($this->name,$list,$where);	
		}
		//если у модели находим собственный инсерт делаем его
		if(method_exists($this,'insert'))return $this->insert($list,$params);
		//иначе делаем нативный insert
		if(!$this->db->insert($this->name,$list,$params)){
			return false;
		}else{
			//возвращем новый id
			return $this->db->lastId();
		}
	}
	/**
	 * Удаляем данные
	 *
	 * @param mixed $id число или массив указывающий на id для перезаписи
	 * @param array $params дополнительные параметры
	 * @return boolean
	 */
	public function delete($id=false,$params=false){
		//смотрим переданы ли условия
		if($where=$this->createWhere($id,$params)){
			if(!$this->db->delete($this->name,$where)){
				return false;
			}
			return true;
		}
		return false;
	}
	/**
	 * Рекурсивное удаление данных
	 *
	 * @param int $id первоначальный элемент
	 * @param string $parent_field имя поля которое содержит id родителя
	 */
	public function deleteRecursive($id, $parent_field){
	    $list=$this->load(false,$this->id_field[0],array('where'=>$parent_field.'='.$id));
	    if(is_array($list))foreach ($list as $item) {
	        //удаляем потомков
	    	$this->deleteRecursive($item[$this->id_field[0]],$parent_field);
	    }
	    //удаляем сам элемент
	    $this->delete($id);
	}
	/**
	 * Создаём условие where на основе id и params['where']
	 *
	 * @param mixed $id число или массив указывающий на id для перезаписи
	 * @param array $params дополнительные параметры
	 * @return string готовое условие
	 */
	private function createWhere($id,$params){
		if($id || isset($params['where'])){
			$where=array();
			if($id){
				//проверяем количество ключевых полей
				if(count($this->id_field)==1){
					//если одно поле то присваиваем всё к нему
					if(is_array($id)){
						if(isset($id[$this->id_field[0]]))$id=$id[$this->id_field[0]];
						else $id=$id[0];
					}
					$where[]=$this->getField($this->fieldByName($this->id_field[0]))->fieldName().'='.$this->db->eraseData($id);
				}else{
					//если много полей то ищем соответствия
					foreach ($this->id_field as $value) {
						if(isset($id[$value]))$where[]=$this->db->apostrof($value).'='.$this->db->eraseData($id[$value]);
					}
				}
				$where=implode(' AND ',$where).(isset($params['where'])?' AND '.$params['where']:'');
			}else{
				$where=$params['where'];
			}
			return $where;	
		}
		return false;
	}
	/**
	 * Добавляем имя таблици и заворачиваем в апострофы
	 *
	 * @param string $field имя поля
	 * @return string
	 */
	public function ap($field){
	    return DB::apostrof($this->name.DOT.$field);
	}
}